
import xhr from '../utils/xhr'
import { getLocation, getQuery } from '../utils/path'
import getData from '../format/index'
import { getListenerData, beforeDispose } from '../utils/fn'
import { isFn } from '../utils/verify'

import {
	getPageCache,
	setPageCache,
	getNodeCache,
	setNodeCache,
} from '../utils/storage'

import {
	report,
	reportEnd,
	getNodeList,
	setNodeList,
	getEventList,
	setEventList,
	formatData,
	getBaseData,
	init,
} from './Symbol'

// 插件相关
import { version } from '../../package.json'
let id = 0

// 构建只读环境
const readOnly = {}
const readOnlyPublic = {
	// 页面缓存
	pageReportHistory: getPageCache(),
	// 节点缓存
	nodeReportHistory: {},
	// 上报存储
	reportCache: [],
	// 监听上报路径
	listenerId: 0,
	listeners: [],
	// 数据处理
	dataPipesCount: 0,
	dataPipes: [],
	// 插件
	initPlugins: [],

	// 添加节点完成
	addNodeHooksCount: 0,
	addNodeHooks: [],

	// 上报完成钩子
	reportHooksCount: 0,
	reportHooks: [],
}

/**
 * @class Stat
 * @description 埋点方法
 * @param {Function} report 上报方法
 * @param {String} url 请求地址
 * @param {Number} offset 路径偏移，用作节点存储的key，如使用 vue 且有 router 并填写了 base url 则传1，如：/xxx/yyy/zzz -> 传 0 时取xxx，传 1 时取 yyy
 * @param {Boolean} showConsole 展示上报数据
 * @param {Boolean} oncePageView 页面展示上报缓存
 * @param {Boolean} onceNodeView 节点展示上报缓存
 * @param {Boolean} closeAutoNodeReport 关闭自动节点展示上报
 * @param {Array} plugins 插件
 */
export default class Stat {
	constructor(
		{
			report: _report = null,
			url = '',
			offset = 0,
			showConsole = true,
			oncePageView = false,
			onceNodeView = false,
			closeAutoNodeReport = false,
			plugins = [],
			...options
		} = {}
	) {
		[
			{ key: 'version', value: version }, // 版本
			{ key: '_id', value: id++ }, // 当前环境插件id
			{ key: 'pathnameOffset', value: offset }, // 如vue有router并填写了 base url 则传1

		].forEach(item => Object.defineProperty(this, item.key, { value: item.value }))

		// 配置参数
		this.options = {
			report: _report,
			url,
			showConsole,
			oncePageView,
			onceNodeView,
			closeAutoNodeReport,
			nextParams: {}, // 默认参数

			...options
		}

		// 只读环境参数
		readOnly[this._id] = {
			currentPath: '',
			lastPath: '',

			// 节点存储列表
			nodeList: {},

			// 节点事件存储列表
			eventList: {},

			// 监听上报路径
			listenerId: 0,
			listeners: [],

			// 数据处理
			dataPipesCount: 0,
			dataPipes: [],

			// 添加节点完成
			addNodeHooksCount: 0,
			addNodeHooks: [],

			// 上报完成钩子
			reportHooksCount: 0,
			reportHooks: [],
		}

		// 初始化插件
		try {
			plugins.forEach(plugin => {
				if (isFn(plugin.apply)) plugin.apply.call(plugin, this)
				else if (isFn(plugin)) plugin.call(null, this)
			})
		} catch (error) {
			console.error('stat plugins error!', error)
		}

		// 初始化
		this[init]()
	}


	/**
	 * 只读参数 --------------------------------------------------------------------------------------------------------------
	 */

	// 当前key
	get currentPath() {
		const id = this._id
		const path = '/' + (getLocation().pathname.split('/')[this.pathnameOffset] || '')

		if (path !== readOnly[id].currentPath) readOnly[id].lastPath = readOnly[id].currentPath

		return readOnly[id].currentPath = path
	}
	// 上一个key
	get lastPath() {
		return readOnly[this._id].lastPath
	}

	/** 
	 * 公共方法 --------------------------------------------------------------------------------------------------------------
	 */

	/**
	 * @method setOptions
	 * @description 设置options
	 */
	setOptions = (options = {}) => {
		Object.assign(this.options, options)

		if (isFn(options.report)) {
			const cache = readOnlyPublic.reportCache

			cache.length && cache.splice(0, cache.length).forEach(datas => options.report(datas))
		}

		return this.options
	}

	/**
	 * @method setNextParams
	 * @description 设置上报默认参数
	  */
	setNextParams = (params = {}) => this.options.nextParams = params

	/**
	 * @method on
	 * @description 监听钩子
	 * @param {String} type 类型： addNode -> 节点添加完成钩子  report -> 上报完成钩子
	 * @return {Function} 执行该函数则移除当前添加的钩子
	*/
	static on = (type, callback) => beforeDispose(readOnlyPublic, `${type}HooksCount`, `${type}Hooks`, { callback })
	on = (type, callback) => beforeDispose(readOnly[this._id], `${type}HooksCount`, `${type}Hooks`, { callback })

	/**
	 * @method listen
	 * @description 监听上报路径，添加默认参数，在获取默认值之前执行
	 * @param {String} reg 匹配路径
	 * @param {Function} pipe 改变单个数据的方法
	 * @return {Function} 执行该函数则移除当前添加的钩子
	 */
	static listen = (reg, pipe) => beforeDispose(readOnlyPublic, 'listenerId', 'listeners', { reg, pipe })
	listen = (reg, pipe) => beforeDispose(readOnly[this._id], 'listenerId', 'listeners', { reg, pipe })


	/**
	 * @method pipe
	 * @description 添加数据处理钩子，在获取默认值之后，在上报之前执行
	 * @param {Function} pipe 改变整个上报数据的方法，该方法接收这次上报的所有数据
	 * @return {Function} 执行该函数则移除当前添加的钩子
	 */
	static pipe = pipe => beforeDispose(readOnlyPublic, 'dataPipesCount', 'dataPipes', { pipe })
	pipe = pipe => beforeDispose(readOnly[this._id], 'dataPipesCount', 'dataPipes', { pipe })

	// /**
	//  * @method use
	//  * @description 添加插件
	//  * @param {Function | Object} 参数为对象时执行对象内apply方法
	//  */
	// static use = (plugin, ...arg) => {
	// 	if (readOnlyPublic.initPlugins.indexOf(plugin) > -1) return Stat

	// 	const _arg = arg.slice()
	// 	_arg.unshift(Stat)

	// 	if (isFn(plugin.apply)) plugin.apply.apply(plugin, _arg)
	// 	else if (isFn(plugin)) plugin.apply(null, _arg)

	// 	readOnlyPublic.initPlugins.push(plugin)

	// 	return Stat
	// }

	/**
	 * @method addNode
	 * @description 添加上报节点
	 * @param {NODE} el 需要绑定的dom节点 
	 * @param {String} id 唯一id
	 * @param {Boolean} unView 该节点不需要展示上报
	 * @param {Boolean} unClick 该节点不需要点击上报
	 * @param {Function} clickEvent 其他绑定方法
	 * @param {Object} params 需要绑定的数据
	 */
	addNode = (el, { id, unView = false, unClick = false, clickEvent, ...params } = {}) => {
		if (!id) return false

		// 添加节点
		this[setNodeList](id, { el, unView, params })

		// 移除旧事件
		el.removeEventListener('click', this[getEventList]()[id])

		// 保存点击事件，便于移除事件
		this[setEventList](id, () => {
			if (unClick) return

			isFn(clickEvent) ? clickEvent(params) : this.clickReport(params)
		})
		el.addEventListener('click', this[getEventList]()[id])

		// 添加节点完成 调用钩子
		clearTimeout(this.addNodeHooksTimer)
		this.addNodeHooksTimer = setTimeout(() => {
			readOnlyPublic.addNodeHooks.forEach(item => typeof item.callback === 'function' && item.callback())
			readOnly[this._id].addNodeHooks.forEach(item => typeof item.callback === 'function' && item.callback())
		}, 800)

		return true
	}

	/**
	 * @method removeNode
	 * @description 移除上报节点
	 * @param {String} id 唯一id
	 */
	removeNode = id => {
		if (!id) return false
		const nodes = this[getNodeList]()
		const events = this[getEventList]()

		nodes[id] && nodes[id].el.removeEventListener('click', events[id])

		delete events[id]
		delete nodes[id]

		return true
	}

	/**
	 * @method nodeReport
	 * @description 节点上报
	 */
	nodeReport = (...arg) => {
		const path = this.currentPath
		const lastPath = this.lastPath
		const history = readOnlyPublic.nodeReportHistory
		const once = this.options.onceNodeView

		if (once) {
			// 获取节点上报历史记录
			history[path] = getNodeCache(path)
		} else {
			// 清除上一个页面的缓存
			if (history[lastPath]) history[lastPath] = null

			// 当前页面赋值
			if (!history[path]) history[path] = {}
		}

		const nodes = this[getNodeList]()

		const ids = Object.keys(nodes)
		const { innerHeight, innerWidth } = window

		// 需要上报的列表
		const reportList = []

		ids.forEach(id => {
			// 已上传过
			if (history[path][id]) return delete nodes[id]

			const { el, unView, params } = nodes[id]
			if (unView) return
			if (!el.getBoundingClientRect) return

			const { left, top } = el.getBoundingClientRect()
			const { offsetWidth, offsetHeight } = el

			if (
				// 在窗口内
				left >= 0 - offsetWidth / 3 * 2 &&
				left <= innerWidth - offsetWidth / 3 &&
				top >= 0 - offsetHeight / 3 * 2 &&
				top <= innerHeight - offsetHeight / 3
			) {
				reportList.push(params)
				delete nodes[id]
				history[path][id] = true
			}
		})
		// 节点纪录展示缓存
		once && setNodeCache(path, history[path])

		return this[report](reportList.map(params => this[getBaseData]({ action: 'view', ...params })), ...arg)
	}

	/**
	 * @method pageReport
	 * @description 页面上报
	 * @param {Object} params 上报所需参数
	 */
	pageReport = (params, ...arg) => {
		// 缓存页面纪录
		if (this.options.oncePageView) {
			const history = readOnlyPublic.pageReportHistory

			if (history[params.page]) return

			history[params.page] = true
			setPageCache(history)
		}

		return this[report]([this[getBaseData]({ action: 'view', ...params })], ...arg)
	}

	/**
	 * @method clickReport
	 * @description 点击上报
	 * @param {Object} params 上报所需参数
	 */
	clickReport = (params, ...arg) => {

		return this[report]([this[getBaseData]({ action: 'click', ...params })], ...arg)
	}

	/**
	 * @method getCurrentData
	 * @description 获取当前默认数据
	 */
	getCurrentData = params => this[formatData]([this[getBaseData](params)]);


	/**
	 * 私有方法 --------------------------------------------------------------------------------------------------------------
	 */

	/**
	 * 上报
	 */
	[reportEnd](fn, sourceData) {
		return new Promise(resolve => resolve(fn()))
			.then(res => {
				readOnlyPublic.reportHooks.forEach(item => isFn(item.callback) && item.callback(null, res, sourceData))
				readOnly[this._id].reportHooks.forEach(item => isFn(item.callback) && item.callback(null, res, sourceData))

				return res
			})
			.catch(err => {
				readOnlyPublic.reportHooks.forEach(item => isFn(item.callback) && item.callback(err, null, sourceData))
				readOnly[this._id].reportHooks.forEach(item => isFn(item.callback) && item.callback(err, null, sourceData))

				return err
			})
	}

	/**
	 * 上报入口
	 */
	[report](datas, ...arg) {
		const _datas = this[formatData](datas)

		if (!_datas.length) return

		const cache = readOnlyPublic.reportCache
		const { report: _report, url, showConsole } = this.options

		showConsole && console.log('Stat datas: ', _datas)

		if (isFn(_report)) {
			cache.length && cache.splice(0, cache.length).forEach(__datas => _report(__datas))

			return this[reportEnd](() => _report(_datas, ...arg), [_datas, ...arg])
		}
		else if (url) return this[reportEnd](() => xhr({ method: 'POST', url, data: JSON.stringify(_datas) }), [_datas, ...arg])
		else cache.push(_datas)
	}


	/**
	 * 数据处理
	 * @param {Array} datas 数据源列表
	 */
	[formatData](datas) {
		const publicData = readOnlyPublic.dataPipes.reduce((a, b) => isFn(b.pipe) ? b.pipe(a) || a : a, datas)
		const ownData = readOnly[this._id].dataPipes.reduce((a, b) => isFn(b.pipe) ? b.pipe(a) || a : a, publicData)

		return ownData
	}

	/**
	 * 获取数据
	 * @param {Object} params 上报所需参数
	 */
	[getBaseData](params) {
		const currentPath = this.currentPath
		const { pathname } = getLocation()
		const currentQuery = getQuery()

		const publicParams = getListenerData(readOnlyPublic, { ...this.options.nextParams, ...params }, { currentPath, pathname, currentQuery })
		const ownParams = getListenerData(readOnly[this._id], publicParams, { currentPath, pathname, currentQuery })

		return getData(ownParams)
	}


	/**
	 * 当前key节点列表
	 */
	[getNodeList]() {
		const currentPath = this.currentPath
		const id = this._id

		return readOnly[id].nodeList[currentPath] || {}
	}

	/**
	 * 设置当前key节点列表
	 * @param {String} nodeId 唯一id
	 * @param {Node} el 节点对象
	 */
	[setNodeList](nodeId, { el, ...params }) {
		const currentPath = this.currentPath
		const lastPath = this.lastPath
		const id = this._id
		const nodes = readOnly[id].nodeList

		if (nodes[lastPath]) nodes[lastPath] = null
		if (!nodes[currentPath]) nodes[currentPath] = {}

		return nodes[currentPath][nodeId] = { el, ...params }
	}

	/**
	 * 获取当前key事件列表
	 */
	[getEventList]() {
		const currentPath = this.currentPath
		const id = this._id

		return readOnly[id].eventList[currentPath] || {}
	}

	/**
	 * 设置当前key事件列表
	 * @param {String} nodeId 唯一id
	 * @param {Function} event 事件
	 */
	[setEventList](nodeId, event) {
		const currentPath = this.currentPath
		const lastPath = this.lastPath
		const id = this._id
		const events = readOnly[id].eventList

		if (events[lastPath]) events[lastPath] = null
		if (!events[currentPath]) events[currentPath] = {}

		return events[currentPath][nodeId] = event
	}

	/**
	 * 初始化 
	*/
	[init]() {
		if (!this.options.closeAutoNodeReport) {
			// 监听全局scroll，做节点展示自动上报
			let scrollTimer = null
			document.addEventListener('scroll', () => {
				clearTimeout(scrollTimer)
				scrollTimer = setTimeout(() => this.nodeReport(), 800)
			}, {
					capture: true,
					once: false,
					passive: true,
				})

			// 添加 节点添加完成钩子
			this.on('addNode', () => this.nodeReport())
		}
	}
}